001 /*
002 * Copyright 2003-2005 The Apache Software Foundation
003 * Copyright 2005 Stephen McConnell
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package net.dpml.cli.option;
018
019 import java.util.Collections;
020 import java.util.Comparator;
021 import java.util.List;
022 import java.util.ListIterator;
023 import java.util.Set;
024 import java.util.StringTokenizer;
025
026 import net.dpml.cli.Argument;
027 import net.dpml.cli.DisplaySetting;
028 import net.dpml.cli.HelpLine;
029 import net.dpml.cli.Option;
030 import net.dpml.cli.OptionException;
031 import net.dpml.cli.WriteableCommandLine;
032 import net.dpml.cli.resource.ResourceConstants;
033 import net.dpml.cli.resource.ResourceHelper;
034 import net.dpml.cli.validation.InvalidArgumentException;
035 import net.dpml.cli.validation.Validator;
036
037 /**
038 * An implementation of an Argument.
039 * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
040 * @version 1.0.0
041 */
042 public class ArgumentImpl extends OptionImpl implements Argument
043 {
044 private static final char NUL = '\0';
045
046 /**
047 * The default value for the initial separator char.
048 */
049 public static final char DEFAULT_INITIAL_SEPARATOR = NUL;
050
051 /**
052 * The default value for the subsequent separator char.
053 */
054 public static final char DEFAULT_SUBSEQUENT_SEPARATOR = NUL;
055
056 /**
057 * The default token to indicate that remaining arguments should be consumed
058 * as values.
059 */
060 public static final String DEFAULT_CONSUME_REMAINING = "--";
061
062 private final String m_name;
063 private final String m_description;
064 private final int m_minimum;
065 private final int m_maximum;
066 private final char m_initialSeparator;
067 private final char m_subsequentSeparator;
068 private final boolean m_subsequentSplit;
069 private final Validator m_validator;
070 private final String m_consumeRemaining;
071 private final List m_defaultValues;
072 private final ResourceHelper m_resources = ResourceHelper.getResourceHelper();
073
074 /**
075 * Creates a new Argument instance.
076 *
077 * @param name the name of the argument
078 * @param description a description of the argument
079 * @param minimum the minimum number of values needed to be valid
080 * @param maximum the maximum number of values allowed to be valid
081 * @param initialSeparator the char separating option from value
082 * @param subsequentSeparator the char separating values from each other
083 * @param validator object responsible for validating the values
084 * @param consumeRemaining String used for the "consuming option" group
085 * @param valueDefaults values to be used if none are specified.
086 * @param id the id of the option, 0 implies automatic assignment.
087 *
088 * @see OptionImpl#OptionImpl(int,boolean)
089 */
090 public ArgumentImpl(
091 final String name, final String description, final int minimum, final int maximum,
092 final char initialSeparator, final char subsequentSeparator, final Validator validator,
093 final String consumeRemaining, final List valueDefaults, final int id )
094 {
095 super( id, false );
096
097 m_description = description;
098 m_minimum = minimum;
099 m_maximum = maximum;
100 m_initialSeparator = initialSeparator;
101 m_subsequentSeparator = subsequentSeparator;
102 m_subsequentSplit = subsequentSeparator != NUL;
103 m_validator = validator;
104 m_consumeRemaining = consumeRemaining;
105 m_defaultValues = valueDefaults;
106
107 if( null == name )
108 {
109 m_name = "arg";
110 }
111 else
112 {
113 m_name = name;
114 }
115
116 if( m_minimum > m_maximum )
117 {
118 throw new IllegalArgumentException(
119 m_resources.getMessage(
120 ResourceConstants.ARGUMENT_MIN_EXCEEDS_MAX ) );
121 }
122
123 if( ( m_defaultValues != null ) && ( m_defaultValues.size() > 0 ) )
124 {
125 if( valueDefaults.size() < minimum )
126 {
127 throw new IllegalArgumentException(
128 m_resources.getMessage(
129 ResourceConstants.ARGUMENT_TOO_FEW_DEFAULTS ) );
130 }
131 if( m_defaultValues.size() > maximum )
132 {
133 throw new IllegalArgumentException(
134 m_resources.getMessage(
135 ResourceConstants.ARGUMENT_TOO_MANY_DEFAULTS ) );
136 }
137 }
138 }
139
140 /**
141 * The preferred name of an option is used for generating help and usage
142 * information.
143 *
144 * @return The preferred name of the option
145 */
146 public String getPreferredName()
147 {
148 return m_name;
149 }
150
151 /**
152 * Processes the "README" style element of the argument.
153 *
154 * Values identified should be added to the CommandLine object in
155 * association with this Argument.
156 *
157 * @see WriteableCommandLine#addValue(Option,Object)
158 *
159 * @param commandLine The CommandLine object to store results in.
160 * @param arguments The arguments to process.
161 * @param option The option to register value against.
162 * @throws OptionException if any problems occur.
163 */
164 public void processValues(
165 final WriteableCommandLine commandLine, final ListIterator arguments, final Option option )
166 throws OptionException
167 {
168 int argumentCount = commandLine.getValues( option, Collections.EMPTY_LIST ).size();
169
170 while( arguments.hasNext() && ( argumentCount < m_maximum ) )
171 {
172 final String allValues = stripBoundaryQuotes( (String) arguments.next() );
173
174 // should we ignore things that look like options?
175 if( allValues.equals( m_consumeRemaining ) )
176 {
177 while( arguments.hasNext() && ( argumentCount < m_maximum ) )
178 {
179 ++argumentCount;
180 commandLine.addValue( option, arguments.next() );
181 }
182 }
183 // does it look like an option?
184 else if( commandLine.looksLikeOption( allValues ) )
185 {
186 arguments.previous();
187 break;
188 }
189 // should we split the string up?
190 else if( m_subsequentSplit )
191 {
192 final StringTokenizer values =
193 new StringTokenizer( allValues, String.valueOf( m_subsequentSeparator ) );
194 arguments.remove();
195
196 while( values.hasMoreTokens() && ( argumentCount < m_maximum ) )
197 {
198 ++argumentCount;
199 final String token = values.nextToken();
200 commandLine.addValue( option, token );
201 arguments.add( token );
202 }
203
204 if( values.hasMoreTokens() )
205 {
206 throw new OptionException(
207 option,
208 ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
209 values.nextToken() );
210 }
211 }
212 else
213 {
214 // it must be a value as it is
215 ++argumentCount;
216 commandLine.addValue( option, allValues );
217 }
218 }
219 }
220
221 /**
222 * Indicates whether this Option will be able to process the particular
223 * argument.
224 *
225 * @param commandLine the CommandLine object to store defaults in
226 * @param argument the argument to be tested
227 * @return true if the argument can be processed by this Option
228 */
229 public boolean canProcess( final WriteableCommandLine commandLine, final String argument )
230 {
231 return true;
232 }
233
234 /**
235 * Identifies the argument prefixes that should be considered options. This
236 * is used to identify whether a given string looks like an option or an
237 * argument value. Typically an option would return the set [--,-] while
238 * switches might offer [-,+].
239 *
240 * The returned Set must not be null.
241 *
242 * @return The set of prefixes for this Option
243 */
244 public Set getPrefixes()
245 {
246 return Collections.EMPTY_SET;
247 }
248
249 /**
250 * Processes String arguments into a CommandLine.
251 *
252 * The iterator will initially point at the first argument to be processed
253 * and at the end of the method should point to the first argument not
254 * processed. This method MUST process at least one argument from the
255 * ListIterator.
256 *
257 * @param commandLine the CommandLine object to store results in
258 * @param args the arguments to process
259 * @throws OptionException if any problems occur
260 */
261 public void process( WriteableCommandLine commandLine, ListIterator args )
262 throws OptionException
263 {
264 processValues( commandLine, args, this );
265 }
266
267 /**
268 * Returns the initial separator character or
269 * '\0' if no character has been set.
270 *
271 * @return char the initial separator character
272 */
273 public char getInitialSeparator()
274 {
275 return m_initialSeparator;
276 }
277
278 /**
279 * Returns the subsequent separator character.
280 *
281 * @return the subsequent separator character
282 */
283 public char getSubsequentSeparator()
284 {
285 return m_subsequentSeparator;
286 }
287
288 /**
289 * Identifies the argument prefixes that should trigger this option. This
290 * is used to decide which of many Options should be tried when processing
291 * a given argument string.
292 *
293 * The returned Set must not be null.
294 *
295 * @return The set of triggers for this Option
296 */
297 public Set getTriggers()
298 {
299 return Collections.EMPTY_SET;
300 }
301
302 /**
303 * Return the consume remaining flag.
304 * @return the consume remaining flag
305 */
306 public String getConsumeRemaining()
307 {
308 return m_consumeRemaining;
309 }
310
311 /**
312 * Return the list of default values.
313 * @return the default values
314 */
315 public List getDefaultValues()
316 {
317 return m_defaultValues;
318 }
319
320 /**
321 * Return the argument validator.
322 * @return the validator
323 */
324 public Validator getValidator()
325 {
326 return m_validator;
327 }
328
329 /**
330 * Performs any necessary validation on the values added to the
331 * CommandLine.
332 *
333 * Validation will typically involve using the
334 * CommandLine.getValues(option) method to retrieve the values
335 * and then either checking each value. Optionally the String
336 * value can be replaced by another Object such as a Number
337 * instance or a File instance.
338 *
339 * @see net.dpml.cli.CommandLine#getValues(Option)
340 *
341 * @param commandLine The CommandLine object to query.
342 * @throws OptionException if any problems occur.
343 */
344 public void validate( final WriteableCommandLine commandLine ) throws OptionException
345 {
346 validate( commandLine, this );
347 }
348
349 /**
350 * Performs any necessary validation on the values added to the
351 * CommandLine.
352 *
353 * Validation will typically involve using the
354 * CommandLine.getValues(option) method to retrieve the values
355 * and then either checking each value. Optionally the String
356 * value can be replaced by another Object such as a Number
357 * instance or a File instance.
358 *
359 * @see net.dpml.cli.CommandLine#getValues(Option)
360 *
361 * @param commandLine The CommandLine object to query.
362 * @param option The option to lookup values with.
363 * @throws OptionException if any problems occur.
364 */
365 public void validate(
366 final WriteableCommandLine commandLine, final Option option )
367 throws OptionException
368 {
369 final List values = commandLine.getValues( option );
370 if( values.size() < m_minimum )
371 {
372 throw new OptionException(
373 option,
374 ResourceConstants.ARGUMENT_MISSING_VALUES );
375 }
376
377 if( values.size() > m_maximum )
378 {
379 throw new OptionException(
380 option,
381 ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
382 (String) values.get( m_maximum ) );
383 }
384
385 if( m_validator != null )
386 {
387 try
388 {
389 m_validator.validate( values );
390 }
391 catch( InvalidArgumentException ive )
392 {
393 throw new OptionException(
394 option,
395 ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
396 ive.getMessage() );
397 }
398 }
399 }
400
401 /**
402 * Appends usage information to the specified StringBuffer
403 *
404 * @param buffer the buffer to append to
405 * @param helpSettings a set of display settings @see DisplaySetting
406 * @param comp a comparator used to sort the Options
407 */
408 public void appendUsage(
409 final StringBuffer buffer, final Set helpSettings, final Comparator comp )
410 {
411 // do we display the outer optionality
412 final boolean optional = helpSettings.contains( DisplaySetting.DISPLAY_OPTIONAL );
413
414 // allow numbering if multiple args
415 final boolean numbered =
416 ( m_maximum > 1 )
417 && helpSettings.contains( DisplaySetting.DISPLAY_ARGUMENT_NUMBERED );
418
419 final boolean bracketed = helpSettings.contains( DisplaySetting.DISPLAY_ARGUMENT_BRACKETED );
420
421 // if infinite args are allowed then crop the list
422 final int max = getMaxValue();
423
424 int i = 0;
425
426 // for each argument
427 while( i < max )
428 {
429 // if we're past the first add a space
430 if( i > 0 )
431 {
432 buffer.append( ' ' );
433 }
434
435 // if the next arg is optional
436 if( ( i >= m_minimum ) && ( optional || ( i > 0 ) ) )
437 {
438 buffer.append( '[' );
439 }
440
441 if( bracketed )
442 {
443 buffer.append( '<' );
444 }
445
446 // add name
447 buffer.append( m_name );
448 ++i;
449
450 // if numbering
451 if( numbered )
452 {
453 buffer.append( i );
454 }
455
456 if( bracketed )
457 {
458 buffer.append( '>' );
459 }
460 }
461
462 // if infinite args are allowed
463 if( m_maximum == Integer.MAX_VALUE )
464 {
465 // append elipsis
466 buffer.append( " ..." );
467 }
468
469 // for each argument
470 while( i > 0 )
471 {
472 --i;
473 // if the next arg is optional
474 if( ( i >= m_minimum ) && ( optional || ( i > 0 ) ) )
475 {
476 buffer.append( ']' );
477 }
478 }
479 }
480
481 /**
482 * Returns a description of the option. This string is used to build help
483 * messages as in the HelpFormatter.
484 *
485 * @see net.dpml.cli.util.HelpFormatter
486 * @return a description of the option.
487 */
488 public String getDescription()
489 {
490 return m_description;
491 }
492
493 /**
494 * Builds up a list of HelpLineImpl instances to be presented by HelpFormatter.
495 *
496 * @see HelpLine
497 * @see net.dpml.cli.util.HelpFormatter
498 * @param depth the initial indent depth
499 * @param helpSettings the HelpSettings that should be applied
500 * @param comp a comparator used to sort options when applicable.
501 * @return a List of HelpLineImpl objects
502 */
503 public List helpLines( final int depth, final Set helpSettings, final Comparator comp )
504 {
505 final HelpLine helpLine = new HelpLineImpl( this, depth );
506 return Collections.singletonList( helpLine );
507 }
508
509 /**
510 * Retrieves the maximum number of values acceptable for a valid Argument
511 *
512 * @return the maximum number of values
513 */
514 public int getMaximum()
515 {
516 return m_maximum;
517 }
518
519 /**
520 * Retrieves the minimum number of values required for a valid Argument
521 *
522 * @return the minimum number of values
523 */
524 public int getMinimum()
525 {
526 return m_minimum;
527 }
528
529 /**
530 * If there are any leading or trailing quotes remove them from the
531 * specified token.
532 *
533 * @param token the token to strip leading and trailing quotes
534 * @return String the possibly modified token
535 */
536 public String stripBoundaryQuotes( String token )
537 {
538 if( !token.startsWith( "\"" ) || !token.endsWith( "\"" ) )
539 {
540 return token;
541 }
542 token = token.substring( 1, token.length() - 1 );
543 return token;
544 }
545
546 /**
547 * Indicates whether argument values must be present for the CommandLine to
548 * be valid.
549 *
550 * @see #getMinimum()
551 * @see #getMaximum()
552 * @return true iff the CommandLine will be invalid without at least one
553 * value
554 */
555 public boolean isRequired()
556 {
557 return getMinimum() > 0;
558 }
559
560 /**
561 * Adds defaults to a CommandLine.
562 *
563 * @param commandLine the CommandLine object to store defaults in.
564 */
565 public void defaults( final WriteableCommandLine commandLine )
566 {
567 super.defaults( commandLine );
568 defaultValues( commandLine, this );
569 }
570
571 /**
572 * Adds defaults to a CommandLine.
573 *
574 * @param commandLine the CommandLine object to store defaults in.
575 * @param option the Option to store the defaults against.
576 */
577 public void defaultValues( final WriteableCommandLine commandLine, final Option option )
578 {
579 commandLine.setDefaultValues( option, m_defaultValues );
580 }
581
582 private int getMaxValue()
583 {
584 if( m_maximum == Integer.MAX_VALUE )
585 {
586 return 2;
587 }
588 else
589 {
590 return m_maximum;
591 }
592 }
593
594 }